Skip to content

Keep backslash-escaped pipes inside table cells - #818

Merged
mattr- merged 1 commit into
vmg:masterfrom
flavorjones:card-604-table-escaped-pipes
Oct 8, 2026
Merged

mattr- merged 1 commit into
vmg:masterfrom
flavorjones:card-604-table-escaped-pipes

Conversation

@flavorjones

Copy link
Copy Markdown
Contributor

Per the GitHub Flavored Markdown spec, a table cell can contain a pipe written as \|, even inside a code span (example 200). parse_table_row in ext/redcarpet/markdown.c splits a row at every |, so the cell ends at the escaped pipe. Inside a code span, the split also ends the span early, and Redcarpet parses the rest of the cell as Markdown. Given the input:

| a | b |
| - | - |
| `x\|[click](javascript:alert(1))` | y |

Redcarpet previously produced these cells:

<td>`x\</td>
<td><a href="javascript:alert(1)">click</a>`</td>

Now it produces the cells a compliant parser emits, matching cmark-gfm:

<td><code>x|[click](javascript:alert(1))</code></td>
<td>y</td>

The table parser removes the backslash before inline parsing, because the CommonMark spec treats backslashes inside a code span as literal text. parse_table_header also counted \| as a column separator, so a header containing one gained an extra column and Redcarpet rendered the whole table as a paragraph. parse_table_header now skips \|, too.

A pipe after any backslash, including an escaped one (\\|), stays in the cell, and only the backslash directly before the pipe is removed, as cmark-gfm's unescape_pipes does. This rule deliberately differs from the backslash-parity rule #817 added for link labels. A tool that escapes every | in a code span as \| turns code containing \| into `\\|`, and only the cmark-gfm rule keeps that code span in one piece.

Fixes #477.

Comment thread ext/redcarpet/markdown.c
Comment on lines +2279 to +2284
static int
is_table_delimiter(uint8_t *data, size_t i)
{
return data[i] == '|' && (i == 0 || data[i - 1] != '\\');
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate this abstraction!

Comment thread ext/redcarpet/markdown.c Outdated
parse_inline(cell_work, rndr, cell_text->data, cell_text->size);
rndr->cb.table_cell(row_work, cell_work, col_data[col] | header_flag, rndr->opaque);

rndr_popbuf(rndr, BUFFER_SPAN);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this additional rndr_popbuf call intentional? It's not clear why we would need two calls.

@flavorjones flavorjones Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattr- Yes, this is intentional! Each cell now takes two span buffers, cell_work and cell_text, so it needs two pops. I added a comment to each pop to make that a bit more clear.

Redcarpet split table rows at every pipe, including `\|`. Per GFM, `\|`
belongs to the cell, even inside a code span. A split inside a code
span ended the span early, so Redcarpet parsed the rest of the cell as
Markdown, links included.

Skip `\|` when splitting cells and counting header columns, and remove
the backslash before inline parsing, as `cmark-gfm` does.

Fixes vmg#477.
@flavorjones
flavorjones force-pushed the card-604-table-escaped-pipes branch from 2b6fbae to b3c4099 Compare October 8, 2026 18:11
@mattr-
mattr- merged commit 73e9141 into vmg:master Oct 8, 2026
10 checks passed
@flavorjones
flavorjones deleted the card-604-table-escaped-pipes branch October 10, 2026 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can't escape a pipe character in tables.

2 participants